<?php
declare(strict_types=1);

namespace WebApp\lib\db;

use ErrorException;
use Exception;
use PDO;
use PDOException;
use ServerApp\lib\core\ServerStation;
use SplQueue;
use Swoole\Coroutine;
use Swoole\Coroutine\Mysql;
use txz\common\dao\models\data\txz_push\ZUserDao;
use txz\common\third_lib\model\conf\DbConfig;
use txz\common\third_lib\model\util\DbUtil;
use WebApp\lib\db\mysql\IMysqlExecute;
use WebApp\lib\exception\ClientOkException;
use WebApp\lib\util\Util;

/**
 * Class MysqlPool
 * @package server\lib
 */
abstract class MysqlPool
{
    protected $db_name = '';
    private $pool;
    public $use_co = true;

    /**
     * MysqlPool constructor.
     * @param $db_name
     */
    public function __construct($db_name)
    {
        $this->db_name = $db_name;
        if (ServerStation::$wId !== null) {
            $GLOBALS['j_m_limit'] = 50;
//        if (ServerStation::$wId !== null) {
            $this->use_co = true;
            $this->pool = new SplQueue;
        } else {
            $this->use_co = false;
        }
    }

    /**
     * @return PDO
     */
    abstract protected function getCon(): PDO;

    /**
 * @return string
 */
    abstract protected function getHost(): string;

    /**
     * @return int
     */
    abstract protected function getHostFlag(): int;

    /**
     * @return int
     */
    abstract protected function getPort(): int;

    /**
     * @return string
     */
    abstract protected function getUser(): string;

    /**
     * @return string
     */
    abstract protected function getPwd(): string;

    /**
     * @param Mysql $client
     */
    private function put(Mysql $client)
    {
        $this->pool->push($client);
    }

    /**
     * @param $func
     * @param Table $executor
     * @return mixed
     * @throws ClientOkException
     */
    public function exec($func, Table $executor)
    {
        if (!$executor->getPool()->use_co) {
            for($i=0; $i < 2; $i++) {
                $query = function() use($executor, $func){
                    $start = microtime(true);
                    $pdo_func = function() {
                        return $this->getCon();
                    };
                    $dao_instance = DbConfig::getInstance()->getDaoInstance($this->getHostFlag());
                    $exe = new class ($pdo_func, $dao_instance) implements IMysqlExecute{
                        /**
                         * @var PDO
                         */
                        private $pdo;

                        /**
                         * @var callable
                         */
                        private $pdo_func;
                        /**
                         * @var string
                         */
                        private $dao_instance;

                        public function __construct(callable $pdo_func, string $dao_instance)
                        {
                            $this->pdo_func = $pdo_func;
                            $this->dao_instance = $dao_instance;
                        }

                        public function getPdo(): PDO
                        {
                            if (!$this->pdo) {
                                $this->pdo = call_user_func_array($this->pdo_func, []);
                            }
                            return $this->pdo;
                        }

                        public function getDaoInstance(&$db_str = null): string
                        {
                            //txz_account.device_info
                            if ($db_str) {
                                $need_to_change_key = "db-str-need_to_change({$db_str})";
                                $need_to_change_instance_name = "db-str-need_to_change_instance_name({$db_str})";
                                $need_to_change = Util::get_global($need_to_change_key);
                                if ($need_to_change === null) {
                                    $arr = explode('.', $db_str);
                                    $db_name = $arr[0];
                                    $table_name = $arr[1];
                                    $new_name = DbUtil::tableToClass($table_name);
                                    if (strncmp($new_name, 'ZUser', 5) == 0) {
                                        $new_name = 'ZUser';
                                    }
                                    $class_name = "\\txz\\common\\dao\\models\\data\\{$db_name}\\{$new_name}Dao";
                                    $cmd = "return (new $class_name)->getDbInstanceNamePublic();";

//                                    $cmd = "";
                                    try {
                                        $reflection = new \ReflectionClass($class_name);
                                        $instance = $reflection->newInstance();
                                        if ($cmd) {
                                            $dao_instance_name = eval($cmd);
                                            $dao_instance_name = (string)$dao_instance_name;
                                        } else {
                                            $method = $reflection->getmethod('getDbInstanceName');
                                            $method->setAccessible(true);
                                            $dao_instance_name = $method->invoke($instance);
                                        }
                                    } catch (\Throwable $e) {
                                        $dao_instance_name = "";
                                        $err_msg = $e->getMessage();
                                        if (!Util::hostIsTest()) {
                                            Util::logW($err_msg);
                                        } else {
                                            if (!Util::hasString($err_msg, 'Dao does not exist')) {
                                                Util::logW($err_msg);
                                            }
                                        }
                                    }

                                    Util::set_global($need_to_change_instance_name, $dao_instance_name);

                                    if (Util::hasString($dao_instance_name, 'DrdsDb')) {
                                        $need_to_change = 1;
                                    } else {
                                        $need_to_change = 0;
                                    }
                                    Util::set_global($need_to_change_key, $need_to_change);
                                }

                                if ($need_to_change == 1) {
                                    $arr = explode('.', $db_str);
                                    $db_name = $arr[0];
                                    $table_name = $arr[1];
                                    $db_str = "{$db_name}_drds.{$table_name}";
                                    $dao_instance_name = Util::get_global($need_to_change_instance_name);
                                    if ($dao_instance_name) {
                                        return (string)$dao_instance_name;
                                    } else {
                                        Util::set_global($need_to_change_instance_name, null);
                                    }
                                }
                            }
                            return $this->dao_instance;
                        }
                    };

                    $res = $func($exe);

                    $end = microtime(true);
                    $t_res = ($end - $start) * 1000;
                    if ($t_res > 800) {
                        Util::logI("cost too long $t_res", 0, false);
                        Util::logI($executor->getLastSql(), 0, false);
                    }
                    return $res;
                };

                try {
                    return $query();
                } catch (ClientOkException $e) {
                    throw $e;
                } catch (\Throwable $e) {
                    Util::logD(
                        'sql_error|'. $e->getMessage()
                        . "|" . $e->getCode()
                        . "|" . $executor->getLastSql()
                        . "|" . $executor->getDb()->getLastErrCode()
                        . "|" . $executor->getDb()->getLastErrInfo()
                        . "|" . json_encode($executor->getDb()->getLastParams())
                    );

                    if (Util::hasString($e->getMessage(), "has gone away")) {
                        $executor->getDb()->clear();
                        continue;
                    }
                    Util::logJson(debug_backtrace(), '', null, false);
                    return false;
                }
            }
            return false;
        } else {
            $client_is_ok = true;
            $limit_flag = isset($GLOBALS['j_m_limit']);
            while ($limit_flag) {
                if ($GLOBALS['j_m_limit'] <= 0){

                    Coroutine::sleep(0.1);
                    continue;
                } else {
                    $GLOBALS['j_m_limit']--;
                    break;
                }
            }

            $client = $this->get();
            if (!$client) {
                if($limit_flag) {
                    $GLOBALS['j_m_limit']++;
                }
                return false;
            }

            try {
                $res = $func($client);
                $this->put($client);
                if($limit_flag) {
                    $GLOBALS['j_m_limit']++;
                }
                return $res;
            } catch (PDOException $e) {
                if ($client->errno === 2006) {
                    $client_is_ok = false;
                }
                if ($client_is_ok) {
                    $this->put($client);
                }
                if($limit_flag) {
                    $GLOBALS['j_m_limit']++;
                }
                return false;
            } catch (ErrorException $e) {
                if ($client_is_ok) {
                    $this->put($client);
                }
                if($limit_flag) {
                    $GLOBALS['j_m_limit']++;
                }
                return false;
            } catch (Exception $e) {
                if ($client_is_ok) {
                    $this->put($client);
                }
                if($limit_flag) {
                    $GLOBALS['j_m_limit']++;
                }
                return false;
            }
        }
    }

    /**
     * @return Mysql
     */
    private function get()
    {
        //有空闲连接
        if (count($this->pool) > 0) {
            return $this->pool->pop();
        }

        //无空闲连接，创建新连接
        $client = new Mysql();
        $config = [
            'host' => $this->getHost(),
            'user' => $this->getUser(),
            'password' => $this->getPwd(),
            'database' => $this->db_name,
            'port' => $this->getPort(),
            'timeout'=> 100,
            'charset' => 'utf8mb4'
        ];

        $res = $client->connect(
            $config
        );
        if ($res == false) {
            return null;
        } else {
//            $client->query("set character_set_client=utf8mb4;");
//            $client->query("set character_set_connection=utf8mb4;");
//            $client->query("set character_set_results=utf8mb4;");
//            $client->query("set character_set_database=utf8mb4;");
//            $client->query("set character_set_server=utf8mb4;");
            return $client;
        }
    }
}
